/* 
 * Copyright (C) 2012 Mark Thompson (mark.artifice@gmail.com)
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *  
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *  
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package com.artificetech.videolibrary.metadata;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.channels.FileChannel;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.List;

import com.artificetech.videolibrary.metadata.MediaQuery.VideoDataProvider;
import com.artificetech.videolibrary.metadata.VideoData.ArtProvider;
import com.coremedia.iso.IsoFile;
import com.coremedia.iso.boxes.Box;
import com.coremedia.iso.boxes.ContainerBox;
import com.coremedia.iso.boxes.MetaBox;
import com.coremedia.iso.boxes.MovieBox;
import com.coremedia.iso.boxes.UserDataBox;
import com.coremedia.iso.boxes.apple.AppleAlbumArtistBox;
import com.coremedia.iso.boxes.apple.AppleArtistBox;
import com.coremedia.iso.boxes.apple.AppleCoverBox;
import com.coremedia.iso.boxes.apple.AppleCustomGenreBox;
import com.coremedia.iso.boxes.apple.AppleDataBox;
import com.coremedia.iso.boxes.apple.AppleDescriptionBox;
import com.coremedia.iso.boxes.apple.AppleItemListBox;
import com.coremedia.iso.boxes.apple.AppleRecordingYearBox;
import com.coremedia.iso.boxes.apple.AppleTrackTitleBox;

import android.app.Activity;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.SystemClock;
import android.util.Log;

/**
 * Uses the mp4parser library (http://code.google.com/p/mp4parser/) to extract iTunes-compatible tags from an MP4/M4V file
 * 
 * Handles types video/mp4 and video/x-m4v.
 */
public class MP4VideoDataProvider implements VideoDataProvider {
	private static String TAG="MP4VideoData";
	private static final String[] handlesTypes = { "video/mp4", "video/x-m4v" };
	
	static class MP4Metadata {
		String title=null;
		String titleSort=null;
		int year=-1;
		String description=null;
		String genre=null;
		String cast=null;
		String director=null;
		byte[] coverArt=null;
	}
	
	/**
	 * An implementation of VideoData.ArtProvider that extracts iTunes-style embedded cover art from an MP4 file.
	 */
	private class MP4EmbeddedCoverArt implements ArtProvider {
		/** This works directly on the file itself. */
		private String path;
		/** If there's a decoding problem, chains to any previously added cover art. */
		private ArtProvider fallback;
		
		MP4EmbeddedCoverArt(String _path, ArtProvider _fallback) {
			path=_path;
			fallback=_fallback;
		}
		
		public Bitmap get() {
			try {
				byte[] coverSource=getCoverArt(path);
				return BitmapFactory.decodeByteArray(coverSource, 0, coverSource.length);
			}
			catch (Exception e) {
				Log.e(TAG, "Error retrieving cover art for: "+path, e);
				if (fallback!=null) return fallback.get();
				return null;
			}
		}
	}
	
	private final MP4MetadataCache cache;
	
	public MP4VideoDataProvider(Activity owningActivity) {
		cache=new MP4MetadataCache(owningActivity);
	}

	/** Helper method - assuming there's precisely zero or one child box of the given type, return it or null. */
	private static <T extends Box> T getBox(ContainerBox container, Class<T> type) {
		List<T> matchingBoxes=container.getBoxes(type);
		if (matchingBoxes.isEmpty()) return null;
		return matchingBoxes.get(0);
	}
	
	public boolean handlesType(String mimeType) {
		for (String type : handlesTypes) if (type.equals(mimeType)) return true;
		return false;
	}
	
	private MP4Metadata parse(String path) {
		long startTime=SystemClock.uptimeMillis();
		
		File f=new File(path);
		if (!f.exists()) {
			Log.i(TAG, "File does not exist when parsing metadata from: "+path);
			return null;
		}
		
		long lastModified=f.lastModified();
		
		// Query the database for information on this file.
		MP4Metadata rv=cache.getRecord(path, lastModified);
		if (rv!=null) {
			Log.i(TAG, "Successfully retrieved cached information for: "+path);
			return rv;
		}
		
		IsoFile isoFile=null;
		
		try {
			FileChannel c=new FileInputStream(f).getChannel();
	        isoFile=new IsoFile(c);
		}
		catch (IOException ioe) {
			// Possibly the file has been deleted?
			Log.i(TAG, "Error parsing metadata from: "+path, ioe);
			return null;
		}
		
		try {
			// We want the box with path: moov.udta.meta.ilst...
	        MovieBox moov=getBox(isoFile, MovieBox.class);
	        UserDataBox udta=getBox(moov, UserDataBox.class);
	        MetaBox meta=getBox(udta, MetaBox.class);
	        AppleItemListBox list=getBox(meta, AppleItemListBox.class);
	        
	        rv=new MP4Metadata();
	        
	        // Just for kicks, dump the contents of this metadata
	        Log.i(TAG, list.getBoxes().toString());
		
	        // Do we have a title?
			AppleTrackTitleBox titleBox=getBox(list, AppleTrackTitleBox.class);
			if (titleBox!=null) rv.title=titleBox.getValue();
			// TODO Work out where iTunes stores its sort title field.
			// Do we have a release year?
			AppleRecordingYearBox yearBox=getBox(list, AppleRecordingYearBox.class);
			if (yearBox!=null) rv.year=Integer.parseInt(yearBox.getValue());
			// Do we have a genre?
			AppleCustomGenreBox genreBox=getBox(list, AppleCustomGenreBox.class);
			if (genreBox!=null) rv.genre=genreBox.getGenre();
			// Do we have a description?
			AppleDescriptionBox descriptionBox=getBox(list, AppleDescriptionBox.class);
			if (descriptionBox!=null) rv.description=descriptionBox.getValue();
			// Do we have a cast? (Encoded as comma-seperated values in the artist field)
			AppleArtistBox castBox=getBox(list, AppleArtistBox.class);
			if (castBox!=null) rv.cast=castBox.getValue();
			// Do we have a director? (Encoded as a value (or values) in the album artist field)
			AppleAlbumArtistBox directorBox=getBox(list, AppleAlbumArtistBox.class);
			if (directorBox!=null) rv.director=directorBox.getValue();
			// Do we have cover art?
			AppleCoverBox coverBox=getBox(list, AppleCoverBox.class);
			if (coverBox!=null) {
				AppleDataBox dataBox=getBox(coverBox, AppleDataBox.class);
				if (dataBox!=null) rv.coverArt=dataBox.getData();
			}
		}
    	catch (RuntimeException e) {
    		// Possibly a corrupt file?
    		Log.i(TAG, "Error parsing metadata from: "+path, e);
    	}
    	
		long elapsed=SystemClock.uptimeMillis()-startTime;
		Log.i(TAG, "Metadata parsing took: "+elapsed+" ms");
		
		// Write this to the cache.
		cache.addRecord(path, lastModified, rv);
    	return rv;
	}
	
	public boolean updateInfo(VideoData v) {
		boolean anyUpdated=false;
		MP4Metadata metadata=parse(v.path);
		
	    // Do we have a title?
		if (metadata.title!=null) {
			v.title=metadata.title;
			anyUpdated=true;
		}
		// Do we have a sort title?
		if (metadata.titleSort!=null) {
			v.titleSort=metadata.titleSort;
			anyUpdated=true;
		}
		// Do we have a release year?
		if (metadata.year!=-1) {
			// It's just a year; use Jan 1.
			Calendar c=new GregorianCalendar(metadata.year, 1, 1);
			v.releaseDate=c.getTime();
			anyUpdated=true;
		}
		// Do we have a genre?
		if (metadata.genre!=null) {
			v.genre=metadata.genre;
			anyUpdated=true;
		}
		// Do we have a description?
		if (metadata.description!=null) {
			v.description=metadata.description;
			anyUpdated=true;
		}
		// Do we have a cast?
		if (metadata.cast!=null) {
			v.cast=metadata.cast;
			anyUpdated=true;
		}
		// Do we have a director?
		if (metadata.director!=null) {
			v.director=metadata.director;
			anyUpdated=true;
		}
		// Do we have cover art?
		if (metadata.coverArt!=null) {
			v.coverArt=new MP4EmbeddedCoverArt(v.path, v.coverArt);
			anyUpdated=true;
		}
		
    	return anyUpdated;
	}
	
	/** Extracts the cover art datastream from moov.udta.meta.ilst.covr, which should be in JPEG format */
	private byte[] getCoverArt(String path) throws IOException {
		File f=new File(path);
		if (!f.exists()) throw new FileNotFoundException();
		
		long lastModified=f.lastModified();
		
		// Query the database for information on this file.
		MP4Metadata rv=cache.getRecord(path, lastModified);
		if (rv!=null) return rv.coverArt;
		
		// These *should* all be successful, as we parsed it once, and checked for the presence of cover art, when the file was added.
		FileChannel c=new FileInputStream(f).getChannel();
        IsoFile isoFile=new IsoFile(c);
        MovieBox moov=getBox(isoFile, MovieBox.class);
        UserDataBox udta=getBox(moov, UserDataBox.class);
        MetaBox meta=getBox(udta, MetaBox.class);
        AppleItemListBox list=getBox(meta, AppleItemListBox.class);
        
        AppleCoverBox cover=getBox(list, AppleCoverBox.class);
    	AppleDataBox coverData=getBox(cover, AppleDataBox.class);
    	return coverData.getData();
	}
}
